#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File    :   crontab.py
@Time    :   2024-05-05 14:07
@Author  :   坐公交也用券
@Version :   1.0
@Contact :   faith01238@hotmail.com
@Homepage : https://liumou.site
@Desc    :   对PVE虚拟机进行定时开关机配置
"""
import logging
import os
import subprocess
import sys
from argparse import ArgumentParser
from datetime import datetime, timedelta
from os import path
from sys import exit

import tomllib

# 创建一个自定义的日志记录器
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

# 创建一个自定义的日志处理器，设置其输出格式
formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d | %(funcName)s | %(message)s')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.addHandler(handler)


def list_files(directory):
	"""
	列出指定目录下的所有文件
	"""
	file_list = []
	# 使用glob模块获取指定目录下的所有文件
	for root, dirs, files in os.walk(directory):
		for name in files:
			# print(os.path.join(root, name))
			file_list.append(os.path.join(root, name))
	return file_list


class Crontab:
	def __init__(self, config, config_list: str, debug: bool = False):
		"""
		配置文件
		:param config:
		"""
		self.config_list = config_list
		self.default_config = config
		self.debug = debug
		self.config = config
		self.txt_config = {}
		self.txt_config_list = {}
		self.log_file = None
		# 当前时间:分
		self.current_min = 0
		# 当前时间:时
		self.current_hour = 0
		if self.debug:
			logger.debug("开始实例化")
		# 设置实例配置
		self.toml_name = ""
		self.toml_id = 220
		self.toml_Key1 = ""
		self.toml_Key2 = ""
		# 开启操作
		self.toml_on_hour = 0
		self.toml_on_delayed = 0
		self.toml_on_enabled = False
		# 关闭操作
		self.toml_off_hour = 0
		self.toml_off_delayed = 0
		self.toml_off_enabled = False

	def get_time(self):
		"""
		获取当前时间
		:return:
		"""
		if self.debug:
			logger.debug("正在获取当前时间")
		current = datetime.now()
		self.current_hour = current.hour
		self.current_min = current.minute
		if self.debug:
			logger.debug(f"当前小时: {self.current_hour} - 当前分钟: {self.current_min}")

	def exists(self):
		"""
		判断配置文件是否存在
		:return:
		"""
		if self.debug:
			logger.debug("检查配置文件是否存在")
		if path.exists(self.config):
			return True
		else:
			logger.error(f"配置文件不存在: {self.config}")
			exit(1)

	# def read(self):
	# 	"""
	# 	读取配置文件
	# 	:return:
	# 	"""
	# 	# 判断文件后缀
	# 	if not self.config.endswith(".toml"):
	# 		logger.warning("文件后缀错误,请检查文件后缀是否为.toml")
	# 		return False
	# 	if self.debug:
	# 		logger.debug(f"正在读取配置文件: {self.config}")
	# 	try:
	# 		with open(self.config, "r", encoding='utf-8') as f:
	# 			self.txt_config = toml.load(f)
	# 			return True
	# 	except FileNotFoundError as e:
	# 		logger.error(f"配置文件不存在: {e}")
	# 		return False
	# 	except toml.TomlDecodeError as e:
	# 		logger.error(f"读取配置文件失败,文件格式错误: {e}")
	# 		return False
	# 	except Exception as e:
	# 		print(self.config)
	# 		logger.error(f"读取配置文件失败: {e}")
	# 		return False

	def read_list(self, file):
		"""
		读取配置文件
		:param file: 需要读取的主机配置文件
		:return:
		"""
		# 判断文件后缀
		if not file.endswith(".toml"):
			logger.warning("文件后缀错误,请检查文件后缀是否为.toml")
			return False
		if self.debug:
			logger.debug(f"正在读取配置文件: {file}")
		try:
			with open(file, "rb") as f:
				self.txt_config_list = tomllib.load(f)
				return True
		except FileNotFoundError as e:
			logger.error(f"配置文件不存在: {e}")
			return False
		except tomllib.TOMLDecodeError as e:
			logger.error(f"读取配置文件失败,文件格式错误: {e}")
			return False
		except Exception as e:
			print(file)
			logger.error(f"读取配置文件失败: {e}")
			return False

	def result_log(self, result: str):
		"""
		输出结果
		:return:
		"""
		if self.debug:
			logger.debug("正在输出结果")
		if self.log_file:
			logger.info(f"日志文件: {self.log_file}")
		else:
			logger.info("日志文件: None")
			return
		if not path.isfile(self.log_file):
			os.system(f"touch {self.log_file}")
		try:
			with open(file=self.log_file, mode='a+', encoding='utf-8') as w:
				w.write(result)
		except Exception as e:
			logger.error(f"写入日志文件失败: {e}")

	def pve_on(self, Id: int, name, k1, k2, op: str):
		"""
		pve开机
		:param op: 虚拟机操作
		:param k2: 虚拟机名称关键词2
		:param k1: 虚拟机名称关键词1
		:param name: 虚拟机名称
		:param Id: 虚拟机ID
		:return:
		"""
		cmd_op = "start"
		ops = "开机"
		status_list = ["running".lower(), "stopped".lower()]
		if op.lower() == "off":
			cmd_op = "stop"
			ops = "关机"
		if self.debug:
			logger.debug(f"正在检查开机状态: {Id}")
		cmd = f"qm list | grep -w {Id} |grep -i {name} | grep -i {k1} | grep -i {k2}" + " | awk '{print $3}'"
		if self.debug:
			print(cmd)
		res = subprocess.getstatusoutput(cmd)
		if res[0] == 0:
			res_out = res[1].strip().lower()
			if self.debug:
				logger.debug(f"当前状态: {res_out}")
			if res_out not in status_list:
				logger.error(f"获取状态失败: {Id}")
				print(res[1])
				return
			if cmd_op.lower() == "start".lower() and res_out == "running".lower():
				if self.debug:
					logger.debug(f"{Id} 已经{ops},无需再次操作")
				return
			if cmd_op.lower() == "stop".lower() and res_out == "stopped".lower():
				if self.debug:
					logger.debug(f"{Id} 已经{ops},无需再次操作")
				return
			if self.debug:
				logger.debug(f"正在{ops}: {Id}")
			if os.system(f"qm {cmd_op} {Id}") == 0:
				logger.info(f"{ops}成功: {Id}")
				self.result_log(f"{datetime.now()} - {Id} - {name} - {ops}成功\n")
			else:
				logger.error(f"{ops}失败: {Id}")
				self.result_log(f"{datetime.now()} - {Id} - {name} - {ops}失败\n")
		else:
			logger.error(f"获取状态失败: {Id}")
			print(res[1])

	def get_config_final_time(self, hour, delayed: int):
		"""
		获取配置文件最终时间并计算是否在配置时间内
		:param hour: 配置文件设置的时间
		:param delayed: 配置文件设置的超时时间
		:return: bool(是否需要执行任务)
		"""
		today = datetime.now().date()
		conf_time = datetime.combine(today, datetime.min.time().replace(hour=hour, minute=0, second=0, microsecond=0))
		max_time = conf_time + timedelta(hours=delayed)
		now = datetime.now()
		# 如果当前时间大于配置文件+超时时间,则表示该配置已超时
		if self.debug:
			logger.debug(f"当前时间: {now} - 配置文件时间: {conf_time} - 最大时间: {max_time}")
		if conf_time < now < max_time:
			return True
		else:
			return False

	def operation(self, sec: str, info: dict):
		"""
		执行操作
		:return:
		"""
		try:
			# 获取base信息
			if str(sec).lower() == "base":
				self.toml_name = info['name']
				self.toml_id = info['id']
				self.toml_Key1 = info['Key1']
				self.toml_Key2 = info['Key2']
			else:
				if str(sec).lower() == "on".lower():
					self.toml_on_hour = info['hour']
					self.toml_on_delayed = info['delayed']
					self.toml_on_enabled = info['enabled']
				else:
					if str(sec).lower() == "off".lower():
						self.toml_off_hour = info['hour']
						self.toml_off_delayed = info['delayed']
						self.toml_off_enabled = info['enabled']
					else:
						logger.error(f"未知配置: {sec}")
						exit(1)
		except Exception as e:
			logger.error(f"获取配置参数失败: {e}")
			print(info)
			exit(1)

	# def stand_alone(self):
	# 	"""
	# 	单机配置文件
	# 	:return:
	# 	"""
	# 	cl = [self.default_config]
	# 	if path.isdir(args_conf_dir):
	# 		if self.debug:
	# 			logger.debug(f'正在获取配置文件: {args_conf_dir}')
	# 		for i in list_files(args_conf_dir):
	# 			if self.debug:
	# 				logger.debug(f"正在判断: {i}")
	# 			ff = str(i).split(".")[-1]
	# 			if ff == "toml":
	# 				cl.append(path.join(args_conf_dir, i))
	# 	if self.debug:
	# 		logger.debug(f"当前配置文件数量{len(cl)}:")
	# 	for self.config in cl:
	# 		self.toml_on_enabled = False
	# 		self.toml_off_enabled = False
	# 		if self.debug:
	# 			logger.debug(f"正在读取配置文件: {self.config}")
	# 		self.exists()
	# 		self.read()
	# 		self.get_time()
	# 		if len(self.txt_config) == 0:
	# 			logger.error("配置文件为空")
	# 			exit(1)
	# 		for i in self.txt_config:
	# 			if self.debug:
	# 				logger.debug(self.txt_config[i])
	# 			if str(i) != "log":
	# 				self.operation(info=self.txt_config[i], sec=i)
	#
	# 			if str(i) == "log":
	# 				self.log_file = self.txt_config[i]['file']
	# 		if self.toml_on_enabled:
	# 			if self.get_config_final_time(self.toml_on_hour, self.toml_on_delayed):
	# 				self.pve_on(Id=self.toml_id, name=self.toml_name, k1=self.toml_Key1, k2=self.toml_Key2, op="on")
	# 			else:
	# 				if self.debug:
	# 					logger.debug(f"当前时间不在配置范围： {self.current_hour}:{self.current_min}")
	# 		if self.toml_off_enabled:
	# 			if self.get_config_final_time(self.toml_off_hour, self.toml_off_delayed):
	# 				self.pve_on(Id=self.toml_id, name=self.toml_name, k1=self.toml_Key1, k2=self.toml_Key2, op="off")
	# 			else:
	# 				if self.debug:
	# 					logger.debug(f"当前时间不在配置范围： {self.current_hour}:{self.current_min}")
	# 	sleep(5)

	def pve_on_list(self, Id, action: str, forward_direction: bool):
		"""
		执行列表主机操作
		:param forward_direction: 是否正向执行
		:param Id: 主机ID
		:param action: 配置文件设置的操作
		:return:
		"""
		if not forward_direction:
			logger.info(f"使用逆向执行，正向操作：{action}")
			if action.strip().lower() == "off":
				action = "on"
			else:
				if action.strip().lower() == "on":
					action = "off"
			logger.info(f"逆向操作：{action}")
		logger.info(f"正在执行列表主机操作: {Id} - {action}")
		# 设置两种操作对应的值
		op_dict = {"off": "stop", "on": "start"}
		op_dict_name = {"off": "关机", "on": "开机"}
		# 如果需要进行的操作和对应的当前状态不相等，则执行操作，否则不执行
		status_dict = {"on": "running".lower(), "off": "stopped".lower()}
		res = subprocess.getstatusoutput(f"qm status {Id}| " + "awk '{print $2}'")
		if res[0] == 0:
			res_out = res[1].strip().lower()
			if res_out not in status_dict.values():
				logger.error(f"获取状态失败: {Id}")
				print(status_dict.values())
				print(status_dict)
				print(res_out)
				return
			# 如果当前状态与配置文件设置的操作不一致，则执行操作
			if status_dict[action] != res_out:
				if self.debug:
					logger.debug(f"正在{op_dict_name[action]}: {Id}")
				if os.system(f"qm {op_dict[action]} {Id}") == 0:
					logger.info(f"{op_dict_name[action]}成功: {Id}")
					self.result_log(f"{datetime.now()} - {Id} - {op_dict_name[action]}成功\n")
				else:
					logger.error(f"{op_dict_name[action]}失败: {Id}")
					self.result_log(f"{datetime.now()} - {Id} - {op_dict_name[action]}失败\n")
			else:
				if self.debug:
					logger.debug(
						f"当前状态[ {res_out} ]与配置文件设置的操作[ {action} ] 预期状态一致, 无需执行操作: {Id}")

	def check_time(self, start_hour, start_min, end_hour, end_min):
		"""
		判断时间是否在配置文件设置的时间范围内
		:param start_hour:
		:param start_min:
		:param end_hour:
		:param end_min:
		:return:
		"""
		# 格式化成时间对象
		start_time = datetime.strptime(f"{start_hour}:{start_min}", "%H:%M")
		end_time = datetime.strptime(f"{end_hour}:{end_min}", "%H:%M")
		# 获取当前时间对象，仅获取小时和分钟
		start_time = start_time.strftime("%H:%M")
		end_time = end_time.strftime("%H:%M")
		now_time = datetime.now().strftime("%H:%M")
		if self.debug:
			logger.debug(f"当前时间: {now_time}")
		# 如果当前时间在配置文件设置的时间范围内，则返回True，否则返回False
		if start_time <= now_time <= end_time:
			if self.debug:
				logger.debug(f"当前时间在配置文件设置的时间范围内: {start_time} - {end_time}")
			return True
		else:
			if self.debug:
				logger.debug(f"当前时间不在配置文件设置的时间范围内: {start_time} - {end_time}")
			return False

	def list_config(self):
		"""
		读取列表主机配置文件
		:return:
		"""
		# 判断文件夹是否存在且是否有文件
		if path.isdir(self.config_list):
			fl = list_files(self.config_list)
			if fl:
				if self.debug:
					# 打印文件数量
					logger.debug(f"当前配置文件数量: {len(fl)}")
				for file in fl:
					self.read_list(file=file)
					# 获取需要操作的主机ID列表
					data_list = self.txt_config_list["list"]
					host_list = data_list["list"]
					hour_start_ = data_list["hour_start"]
					min_start_ = data_list["min_start"]
					hour_end_ = data_list["hour_end"]
					min_end = data_list["min_end"]
					action_ = data_list["action"]
					if self.check_time(hour_start_, min_start_, hour_end_, min_end):
						for i in host_list:
							self.pve_on_list(Id=i, action=action_, forward_direction=True)
					else:
						for i in host_list:
							self.pve_on_list(Id=i, action=action_, forward_direction=False)

			else:
				logger.warning(f"配置文件目录为空: {self.config_list}")
		else:
			if self.debug:
				logger.debug(f"配置文件目录不存在: {self.config_list}")

	def start(self):
		while True:
			# self.stand_alone()
			self.list_config()


def install():
	"""
	安装
	:return:
	"""
	logger.info("正在进行安装/更新...")
	url_list = 'https://gitee.com/liumou_site/pvet/raw/master/model/qm/list.toml'
	os.system("mkdir -p /opt/apps/com.liumou.pve.crontab/")
	demo_list = "/opt/apps/com.liumou.pve.crontab/demo_list.toml"
	url_dict = {demo_list: url_list}
	for file in url_dict:
		if path.exists(file):
			logger.info("模板配置文件已存在")
			os.system(f"rm -f {file}")
		res = subprocess.getstatusoutput(f'wget -O {file} {url_dict[file]}')
		if res[0] == 0:
			logger.info(f"[ {file} ]配置文件下载成功")
		else:
			logger.info(f"[ {file} ]配置文件下载失败")
			print(res[1])
			exit(1)
	crontab_url = 'https://gitee.com/liumou_site/pvet/raw/master/model/qm/crontab.py'
	crontab_bin = path.join("/usr/bin/com.liumou.pve.crontab")
	os.system(f"rm -f {crontab_bin}")
	res = subprocess.getstatusoutput(f'wget -O {crontab_bin} {crontab_url}')
	if res[0] == 0:
		os.system(f"chmod 0755 {crontab_bin}")
		logger.info("安装/更新成功")
		service()
		exit(0)
	else:
		logger.error("安装/更新失败")
		print(res[1])
		exit(2)


def service():
	"""

	:return:
	"""
	if not path.isfile("/usr/bin/com.liumou.pve.crontab"):
		logger.error(f"执行文件不存在: /usr/bin/com.liumou.pve.crontab")
		exit(1)
	txt = f"""[Unit]
Description=This is a PVE timing tool

[Service]
#Type=simple
Type=forking
User=root
ExecStart=/usr/bin/com.liumou.pve.crontab -debug
StandardOutput=journal
KillMode=control-group
Restart=always
RestartPreventExitStatus=255
StandardError=inherit
SyslogIdentifier=com.liumou.pve.crontab

[Install]
WantedBy=multi-user.target
Alias=com.liumou.pve.crontab.service
"""
	try:
		with open(file="/etc/systemd/system/com.liumou.pve.crontab.service", mode='w', encoding='utf-8') as w:
			w.write(txt)
	except Exception as e:
		logger.error(f"写入服务文件失败: {e}")
		exit(1)
	if os.system("systemctl daemon-reload") == 0:
		os.system("systemctl enable com.liumou.pve.crontab.service")
		if os.system("systemctl restart com.liumou.pve.crontab.service") == 0:
			logger.info("服务启动成功")
			exit(0)
		else:
			logger.error("服务启动失败...")
			exit(3)
	else:
		logger.error("服务启动失败")
		exit(2)


if __name__ == '__main__':
	conf_default = "/opt/apps/com.liumou.pve.crontab/config.toml"
	conf_default_dir = "/opt/apps/com.liumou.pve.crontab/config/"
	conf_default_list = "/opt/apps/com.liumou.pve.crontab/config_list/"
	os.system(f"mkdir -p /opt/apps/com.liumou.pve.crontab/log/ {conf_default_list} {conf_default_dir}")
	if not path.isfile:
		conf_default = path.join(os.getcwd(), "config.toml")
	arg = ArgumentParser(description='当前脚本版本: 3.1', prog="PVE虚拟机/容器定时开关机")
	h = f"设置单机配置文件路径,默认: {conf_default}"
	arg.add_argument('-c', '--conf', type=str, help=h, default=conf_default, required=False)
	h = f"设置列表配置文件路径,默认: {conf_default_list}"
	arg.add_argument('-cl', '--conf_list', type=str, help=h, default=conf_default_list, required=False)
	h = f"设置更多配置文件所在目录(此目录下所有.toml结尾的文件都会被读取),默认: {conf_default_dir}"
	arg.add_argument('-cd', '--conf_dir', type=str, help=h, default=conf_default_dir, required=False)
	arg.add_argument('-install', action='store_true', help='安装/更新服务')
	arg.add_argument('-f', action='store_true', help='强制更新配置文件')
	arg.add_argument('-debug', action='store_true', help='开启调试模式')
	args = arg.parse_args()
	args_conf = args.conf
	args_conf_list = args.conf_list
	args_install = args.install
	args_debug = args.debug
	args_f = args.f
	args_conf_dir = args.conf_dir
	if args_debug:
		logger.info("开启了调试模式")
	if args_install:
		logger.info("开启了安装/更新功能")
		install()
	if not path.exists(args_conf):
		logger.error(f"配置文件不存在: {args_conf}")
		exit(1)
	c = Crontab(config=args_conf, debug=args_debug, config_list=args_conf_list)
	c.start()
